<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div class="panel">
      <input type="file" id="inputFile" />
      <div id="dragArea"></div>
      <input type="number" id="num" value="1" />
      <div id="itemSample" class="item" draggable="true">item</div>
    </div>
    <div id="operate-wrap">
      <div id="operate">
        <img id="imgPre" />
      </div>
    </div>
    <script
      src="https://cdnjs.cloudflare.com/ajax/libs/mathjs/12.0.0/math.js"
      integrity="sha512-rffRpOvP8/vOkbpVUpjesEh2AI40+pNcMh0LAAdOKBE96pLnJh1IGKGhu/RL5lrmW8fA9p5ph5GkCOlMNXr3eg=="
      crossorigin="anonymous"
      referrerpolicy="no-referrer"
    ></script>
    <script>
      const fileInput = document.getElementById('inputFile')
      const elImgPreview = document.getElementById('imgPre')
      //hack  如果elImgPreview初始没有图片，则操作载入大图时，elImgPreview尺寸变为大图尺寸但像素不可见
      elImgPreview.src =
        "data:image/svg+xml;utf8,%3Csvg display='inline-block' vertical-align='middle' width='1em' height='1em' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024' %3E%3Cpath d='M516.8128 807.867733c5.188267 0 10.4448-0.341333 15.701333-0.9216 50.7904-5.905067 93.866667-40.277333 109.806934-87.620266a7.168 7.168 0 0 0-4.027734-8.874667 7.714133 7.714133 0 0 0-9.6256 2.9696c-0.170667 0.3072-20.343467 31.8464-98.850133 42.120533a265.557333 265.557333 0 0 1-34.030933 2.321067c-55.022933-0.034133-79.530667-20.138667-79.7696-20.309333a7.714133 7.714133 0 0 0-9.8304-0.1024 7.168 7.168 0 0 0-1.570134 9.4208 133.870933 133.870933 0 0 0 112.196267 60.996266zM939.008 336.622933c-0.682667 0-1.297067 0.136533-1.979733 0.136534C891.016533 143.906133 715.229867 0 504.490667 0 287.914667 0 108.100267 151.825067 68.266667 352.699733 29.4912 359.424 0 392.430933 0 432.298667v159.675733c0 44.714667 37.0688 81.032533 82.807467 81.032533 25.8048 0 48.571733-11.810133 63.726933-29.934933 36.625067 93.7984 111.172267 169.403733 205.482667 209.3056 0.238933-0.477867 2.048-3.857067 4.164266-6.519467 1.501867-1.877333 3.140267-3.413333 4.573867-3.413333 1.501867 0 2.833067 0.546133 3.9936 1.365333-21.845333-15.7696-100.693333-96.699733-117.76-209.578666-7.509333-49.732267 30.958933-98.5088 75.707733-106.5984 71.8848-12.970667 143.36-27.716267 215.2448-40.413867 45.6704-8.0896 76.9024-32.3584 95.982934-72.772267 4.471467-9.4208 10.922667-28.535467 13.892266-56.081066a8.8064 8.8064 0 0 1 8.704-7.236267c2.9696 0 5.461333 1.467733 7.168 3.584l1.979734-1.194667c28.330667 39.8336 84.514133 128 92.5696 220.9792 9.250133 106.325333 4.096 179.131733-79.940267 253.610667a6.5536 6.5536 0 0 0-2.218667 4.846933c0 2.321067 1.262933 4.266667 3.072 5.461334 0.7168 0.273067 1.4336 0.682667 2.116267 0.9216 0.546133 0.136533 1.092267 0.341333 1.672533 0.341333 0.546133 0 1.058133-0.2048 1.570134-0.341333 1.194667-0.6144 2.321067-1.297067 3.4816-1.911467 84.992-45.329067 150.186667-121.0368 180.292266-212.104533 12.219733 17.544533 31.300267 30.173867 53.486934 34.577066-35.601067 156.672-179.882667 254.737067-358.673067 269.6192-10.717867-25.736533-37.0688-44.032-68.061867-44.032-40.448 0-73.250133 31.061333-73.250133 69.290667 0 38.229333 32.802133 69.2224 73.250133 69.2224 32.631467 0 59.938133-20.2752 69.4272-48.162133 206.9504-16.315733 372.8384-135.406933 407.4496-320.512 30.6176-12.3904 52.087467-41.437867 52.087467-75.3664v-161.553067c0-45.1584-38.058667-81.783467-84.992-81.783467z m-76.322133 46.421334c-53.9648-141.585067-193.8432-242.756267-358.4-242.756267-163.84 0-303.172267 100.317867-357.649067 240.981333-2.730667-3.208533-5.870933-5.973333-9.0112-8.738133 30.856533-171.4176 183.1936-301.738667 366.865067-301.738667 182.818133 0 334.677333 129.024 366.557866 299.246934-3.1744 4.096-6.075733 8.3968-8.362666 12.970666z' fill='currentColor'%3E%3C/path%3E%3C/svg%3E"
      function setImage(url) {
        console.log('setImg', url)
        console.time('loadImg')
        elImgPreview.onload = () => {
          console.timeEnd('loadImg')
          elImgPreview.onload = null
          elImgPreview.onerror = null
        }
        elImgPreview.onerror = (e) => {
          console.timeEnd('loadImg')
          console.error('loadImg', e)
          elImgPreview.onload = null
          elImgPreview.onerror = null
        }
        elImgPreview.src = url
      }
      fileInput.addEventListener('change', () => {
        const file = fileInput.files[0]
        if (file) {
          fileToDataUrl(file).then((result) => {
            setImage(result)
          })
        }
      })

      function fileToDataUrl(file) {
        return new Promise((resolve, reject) => {
          //异步实现：速度还行 60MB文件 300ms
          // const reader = new FileReader()
          // console.time('read file')
          // reader.readAsDataURL(file)
          // reader.onload = (ev) => {
          //   console.timeEnd('read file')
          //   resolve(ev.target.result)
          // }
          // reader.onerror = (ev) => reject(ev)

          // 同步实现:更快 60MB文件 3ms
          console.time('read file')
          const result = URL.createObjectURL(file)
          console.timeEnd('read file')
          resolve(result)
        })
      }

      //支持拖拽到文件
      const elDragArea = document.getElementById('dragArea')
      fileInput.addEventListener('dragover', (e) => {
        e.preventDefault()
      })
      fileInput.addEventListener('drop', (e) => {
        e.preventDefault()
        e.dataTransfer.files[0]
      })

      //点击时触发 文件浏览:参考 element-plus 的upload 源码
      elDragArea.addEventListener('click', () => fileInput.click())

      // 防止没保存时 退出页面
      // window.addEventListener('beforeunload', (e) => {
      //   e.preventDefault()
      //   e.returnValue = '确定活干完了么？'
      // })

      const elOperateWrap = document.getElementById('operate-wrap')
      const elOperate = document.getElementById('operate')
      const elNum = document.getElementById('num')
      let operateTransform = {
        scale: 1,
        origin: [50, 50],
        translate: [0, 0]
      }
      function setOperate(config, assign) {
        operateTransform = Object.assign({}, config, assign)
        const {
          scale,
          origin: [ox, oy],
          translate: [tx, ty]
        } = operateTransform
        elOperate.style.transformOrigin = `${ox}px ${oy}px`
        elOperate.style.transform = `translate(${tx}px, ${ty}px) scale(${scale})`
        elNum.value = scale
      }
      function transformOperate(scale, pos, from) {
        scale = toFixed(scale, 3)
        if (scale < 0.1) return
        if (scale > 5) return
        setOperate(transform(scale, pos, from))
      }

      const m = math.matrix([])
      function transform(scale, pos, from) {
        const [x, y] = pos
        const [oX, oY] = math.multiply(getInvertM(from), [x, y, 1])
        //oX + tX = x
        const tX = toFixed(x - oX, 3)
        const tY = toFixed(y - oY, 3)
        const result = {
          scale: scale,
          origin: [oX, oY],
          translate: [tX, tY]
        }
        console.log('transform==', result.origin, result.translate)
        return result
      }

      //valid验证： transformOperate的第一个参数scale 大于0时  有: 红点（即 #operate::after ） 的左上角应该在绿框中间（即 #operate-wrap::after ）
      // transformOperate(2, [100, 100], {
      //   scale: 1,
      //   origin: [0, 0],
      //   translate: [0, 0]
      // })

      function toFixed(n, fixed) {
        return +n.toFixed(fixed)
      }
      function dragOperate(pos) {
        const [x, y] = pos
        operateTransform.translate = [x, y]
        elOperate.style.left = x + 'px'
        elOperate.style.top = y + 'px'
      }
      function dragOperate2(pos) {
        setOperate(operateTransform, { translate: pos })
      }
      //滚轮时：缩放
      elOperateWrap.addEventListener('wheel', (e) => {
        e.preventDefault()
        const scale = parseFloat(elNum.value) + e.deltaY / 200
        transformOperate(
          scale,
          [e.pageX - elOperateWrap.offsetLeft, e.pageY - elOperateWrap.offsetTop],
          operateTransform
        )
      })
      //矩阵helper
      function getInvertM({ scale, origin, translate }) {
        let m = math.multiply(getM_translate(translate), getM_scaleAtCenter(scale, origin))
        return math.inv(m)
      }
      function getM_scaleAtCenter(scale, [ox, oy]) {
        return [
          [scale, 0, (1 - scale) * ox],
          [0, scale, (1 - scale) * oy],
          [0, 0, 1]
        ]
      }
      function getM_translate([dx, dy]) {
        return [
          [1, 0, dx],
          [0, 1, dy],
          [0, 0, 1]
        ]
      }
      //拖拽
      useDrag(
        elOperateWrap,
        (e) => {
          console.log('拖拽 start', e.target)
          if (![elImgPreview, elOperateWrap].find((t) => e.target === t)) return false
          return [e.pageX, e.pageY, ...operateTransform.translate]
        },
        (e, start) => {
          console.log('拖拽 ing')
          const { pageX, pageY } = e
          const [pageX0, pageY0, left, top] = start
          dragOperate2([left + pageX - pageX0, top + pageY - pageY0])
        }
      )
      function useDrag(el, onStart, onMove, onUp) {
        let start = null
        const eventConfigs = [
          {
            el,
            e: 'pointerdown',
            handler: (e) => {
              start = onStart(e)
            }
          },
          {
            el,
            e: 'pointermove',
            handler: (e) => {
              if (start) {
                onMove(e, start)
              }
            }
          },
          {
            el,
            e: 'pointerup',
            handler: (e) => {
              if (start) {
                onUp && onUp(e, start)
                start = null
              }
            }
          }
        ]

        return registEvent(eventConfigs)
      }
      function registEvent(eventConfigs) {
        for (const { el, e, handler } of eventConfigs) {
          el.addEventListener(e, handler)
        }
        return function unRegistEvent() {
          for (const { el, e, handler } of eventConfigs) {
            el.removeEventListener(e, handler)
          }
        }
      }

      //复制节点到地图去 :参考 mdn https://mdn.github.io/dom-examples/drag-and-drop/copy-move-DataTransfer.html
      function useDrag2(elDrag, elDrop, onStart, onDrop) {
        let start = null
        return registEvent([
          {
            el: elDrag,
            e: 'dragstart',
            handler: (e) => {
              console.log('dragstart', e)
              start = onStart(e)
            }
          },
          {
            el: elDrag,
            e: 'dragend',
            handler: (e) => {
              start = null
            }
          },
          {
            el: elDrop,
            e: 'dragover',
            handler: (e) => {
              e.preventDefault() //调用了才能触发 drop 事件
            }
          },
          {
            el: elDrop,
            e: 'drop',
            handler: (e) => {
              if (start) {
                onDrop(e, start)
              }
            }
          }
        ])
      }
      const elItem = document.getElementById('itemSample')
      useDrag2(
        elItem,
        elOperateWrap,
        (e) => {
          e.dataTransfer.effectAllowed = 'copyMove'
          return [e.pageX, e.pageY, e.target]
        },
        (e, start) => {
          const [, , cloneFrom] = start
          const { pageX, pageY } = e
          console.log('pagex,y', [pageX, pageY])
          console.log('cloneFrom', cloneFrom)
          var nodeCopy = cloneFrom.cloneNode(false)
          nodeCopy.innerText = cloneFrom.innerText
          nodeCopy.id = ''
          const [x, y] = math.multiply(getInvertM(operateTransform), [pageX, pageY, 1])
          console.log('node Copy:', [x, y])
          nodeCopy.style.left = x + 'px'
          nodeCopy.style.top = y + 'px'
          // nodeCopy.draggable = false
          elOperate.appendChild(nodeCopy)
          useDrag2(
            nodeCopy,
            elOperateWrap,
            (e) => {
              // e.preventDefault()
              e.stopPropagation()
              e.dataTransfer.effectAllowed = 'move'
              console.log('move start', e.target)
              return [e.pageX, e.pageY, e.target]
            },
            (e, start) => {
              const [, , cloneFrom] = start
              const { pageX, pageY } = e
              console.log('pagex,y', [pageX, pageY])
              console.log('cloneFrom', cloneFrom)
              const [x, y] = math.multiply(getInvertM(operateTransform), [pageX, pageY, 1]) //结果错的
              console.log('node Move Drop:', [x, y], [pageX, pageY])
              nodeCopy.style.left = x + 'px'
              nodeCopy.style.top = y + 'px'
            }
          )
        }
      )
      /*
      问题：
      - 2个拖拽同时起作用了： item move ; elOperateWrap drag
        - 后者先被调用
        - 解决: 在后者dragStart时过滤 e.target

      */

      /* todo
        - 拖拽销毁
        - 数据列表
        - 数据与el 互操作
        - 导入导出
      
      */
      /* 其他
        图片预览 看不了psd

        */
    </script>
    <style>
      body {
        margin: 0;
      }
      #dragArea {
        width: 200px;
        height: 200px;
        border: 1px dashed #9f9;
      }
      .panel {
        float: right;
        width: 300px;
        background-color: #99f;
        overflow: hidden;
      }
      #imgPre {
        /* border: 1px solid #ff9; */
        min-width: 100px;
        min-height: 100px;
        pointer-events: none;
        user-select: none;
      }
      #operate-wrap {
        width: calc(100% - 300px);
        height: 100vh;
        overflow: hidden;
        background-color: #ddd;
        position: relative;
      }
      #operate-wrap::after {
        content: '';
        left: 100px;
        top: 100px;
        border: 2px solid #0f0;
        width: 2px;
        height: 2px;
        transform: translate(-50%, -50%);
        position: absolute;
        z-index: 1;
      }
      #operate {
        position: absolute;
        transform-origin: 0 0;
      }
      #operate::after {
        content: '';
        left: 100px;
        top: 100px;
        background: #f00;
        width: 5px;
        height: 5px;
        position: absolute;
        z-index: 1;
      }
      .item {
        background: #ff9;
        /* position: ; */
      }
      #operate .item {
        position: absolute;
      }
    </style>
  </body>
</html>
